package cn.tedu.charging.device.repository;

import cn.tedu.charging.common.pojo.po.ChargingStationPO;
import cn.tedu.charging.common.pojo.query.NearStationsQuery;
import cn.tedu.charging.common.pojo.vo.StationInfoVO;
import cn.tedu.charging.device.mapper.StationMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.GeoOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.domain.geo.GeoLocation;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Repository
@Slf4j
public class StationRepository {
    @Autowired
    private StationMapper stationMapper;
    @Resource
    @Qualifier("redis01")
    private RedisTemplate redisCache;

    public Long getTotal() {
        // select count(*) from charging_station
        return stationMapper.selectCount(null);
    }

    public List<ChargingStationPO> getPage(int pageNo, int pageSize) {
        // pageNo=1,pageSize=1000 第一批的1000 -> select * from charging_station limit 0,1000
        // pageNo=2,pageSize=1000 第二批的1000 -> select * from charging_station limit 1000,1000
        int offset = (pageNo - 1) * pageSize;
        Page<ChargingStationPO> page = new Page<>(pageNo, pageSize);
        Page<ChargingStationPO> pagePos = stationMapper.selectPage(page, null);
        return pagePos.getRecords();
    }

    public List<StationInfoVO> getNearStations(NearStationsQuery query) {
        //提前把操作缓存数据的客户端对象 获取
        GeoOperations geoOps = redisCache.opsForGeo();
        ValueOperations<String, ChargingStationPO> stringOps = redisCache.opsForValue();
        //1.利用中心点 和距离 构造 georadius命令 命中一批 stationId(绑定地理位置 距离中心位移)
        //radius beijing:stations query.lon query.lat 10000 m withcoord withdist
        RedisGeoCommands.GeoRadiusCommandArgs args
                = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().
                includeCoordinates().includeDistance();
        Circle circle = new Circle(
                new Point(query.getLongitude(), query.getLatitude()),
                new Distance(10000.0));
        GeoResults<GeoLocation<Integer>> result = geoOps.radius("beijing:stations", circle, args);
        //取 绑定坐标 距离 每个元素值 都在list元素对象里GeoResult
        List<GeoResult<GeoLocation<Integer>>> targets = result.getContent();
        //2.对geo查询的集合做 映射 利用stationId 读取string类型缓存使用stationName
        //如果targets非空 附近查询是命中数据 将命中的每个包含地理位置 位移 站场id的list对象转化成vo
        if (targets != null && targets.size() > 0) {
            return targets.stream().map(target -> {
                //转化封装 StationInfoVO
                StationInfoVO vo = new StationInfoVO();
                //distance 附加属性 距离位移
                BigDecimal distance = new BigDecimal(target.getDistance().getValue());//米单位换算成千米
                log.debug("查询距离:{}米", distance);
                //除以1000-->新的bigDecimal 3928.6742->3.9286742 保证取2位小数点 四舍五入3.93
                BigDecimal dist = distance.divide(new BigDecimal(1000)).setScale(2, BigDecimal.ROUND_HALF_UP);
                log.debug("查询距离:{}千米", dist);
                vo.setDistance(dist.doubleValue());
                //lng lat geo绑定地理位置
                Point point = target.getContent().getPoint();
                vo.setStationLng(new BigDecimal(point.getX()));
                vo.setStationLat(new BigDecimal(point.getY()));
                //id geo元素值
                vo.setStationId(target.getContent().getName());
                //stationName geo没有 取string缓存 key值 结合stationId 读取po
                ChargingStationPO chargingStationPO = stringOps.get("station:detail:" + vo.getStationId());
                vo.setStationName(chargingStationPO.getStationName());
                return vo;
            }).collect(Collectors.toList());
        } else {
            return null;
        }
    }

    public ChargingStationPO getById(Integer stationId) {
        // 旁路缓存cache-aside读流程
        ValueOperations<String, ChargingStationPO> opsForValue = redisCache.opsForValue();
        // 1. 预热的时候，将站场信息po写入到缓存，但是在仓储层查询缓存未必能命中
        ChargingStationPO po = null;
        po = opsForValue.get("station:detail:" + stationId);  // 如果防止穿透的解决方案选择null值，则缓存是否命中最好调整为判断key值是否存在
        // 2. 判断缓存数据是否命中
        if (po == null) {
            // 数据未命中 缓存中没有，需要从数据库查询
            po = stationMapper.selectById(stationId);
            if(po!=null){
                // 缓存未命中，但数据库命中了，保证缓存再下一次查询时一定会命中
                opsForValue.set("station:detail:" + stationId, po);
            } else {
                // 数据未命中，数据库未命中，发生缓存穿透了（内部收到错误数据，外部进行恶意访问）
                // 解决方案1：存储null值，设置最大过期时间，短时间内查询到null值，则直接返回null，不查询数据库
                opsForValue.set("station:detail:" + stationId, null, 60 * 60 * 24 * 7, TimeUnit.SECONDS);
                // 解决方案2：布隆过滤器
            }
        }
        // 命中直接返回
        return po;
    }
}
